/*
 * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.
 */

/*
 * Copyright 2005 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.sun.org.apache.xerces.internal.impl;

import com.sun.org.apache.xerces.internal.impl.Constants;
import com.sun.org.apache.xerces.internal.impl.io.ASCIIReader;
import com.sun.org.apache.xerces.internal.impl.io.UCSReader;
import com.sun.org.apache.xerces.internal.impl.io.UTF8Reader;
import com.sun.org.apache.xerces.internal.impl.msg.XMLMessageFormatter;
import com.sun.org.apache.xerces.internal.impl.XMLEntityHandler;
import com.sun.org.apache.xerces.internal.impl.validation.ValidationManager;
import com.sun.org.apache.xerces.internal.util.*;
import com.sun.org.apache.xerces.internal.util.URI;
import com.sun.org.apache.xerces.internal.utils.SecuritySupport;
import com.sun.org.apache.xerces.internal.utils.XMLLimitAnalyzer;
import com.sun.org.apache.xerces.internal.utils.XMLSecurityManager;
import com.sun.org.apache.xerces.internal.utils.XMLSecurityPropertyManager;
import com.sun.org.apache.xerces.internal.xni.Augmentations;
import com.sun.org.apache.xerces.internal.xni.XMLResourceIdentifier;
import com.sun.org.apache.xerces.internal.xni.XNIException;
import com.sun.org.apache.xerces.internal.xni.parser.*;
import com.sun.xml.internal.stream.Entity;
import com.sun.xml.internal.stream.StaxEntityResolverWrapper;
import com.sun.xml.internal.stream.StaxXMLInputSource;
import com.sun.xml.internal.stream.XMLEntityStorage;
import java.io.*;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Stack;
import javax.xml.XMLConstants;
import javax.xml.stream.XMLInputFactory;


/**
 * Will keep track of current entity.
 *
 * The entity manager handles the registration of general and parameter
 * entities; resolves entities; and starts entities. The entity manager
 * is a central component in a standard parser configuration and this
 * class works directly with the entity scanner to manage the underlying
 * xni.
 * <p>
 * This component requires the following features and properties from the
 * component manager that uses it:
 * <ul>
 * <li>http://xml.org/sax/features/validation</li>
 * <li>http://xml.org/sax/features/external-general-entities</li>
 * <li>http://xml.org/sax/features/external-parameter-entities</li>
 * <li>http://apache.org/xml/features/allow-java-encodings</li>
 * <li>http://apache.org/xml/properties/internal/symbol-table</li>
 * <li>http://apache.org/xml/properties/internal/error-reporter</li>
 * <li>http://apache.org/xml/properties/internal/entity-resolver</li>
 * </ul>
 *
 * @author Andy Clark, IBM
 * @author Arnaud  Le Hors, IBM
 * @author K.Venugopal SUN Microsystems
 * @author Neeraj Bajaj SUN Microsystems
 * @author Sunitha Reddy SUN Microsystems
 * @version $Id: XMLEntityManager.java,v 1.17 2010-11-01 04:39:41 joehw Exp $
 */
public class XMLEntityManager implements XMLComponent, XMLEntityResolver {

  //
  // Constants
  //

  /**
   * Default buffer size (2048).
   */
  public static final int DEFAULT_BUFFER_SIZE = 8192;

  /**
   * Default buffer size before we've finished with the XMLDecl:
   */
  public static final int DEFAULT_XMLDECL_BUFFER_SIZE = 64;

  /**
   * Default internal entity buffer size (1024).
   */
  public static final int DEFAULT_INTERNAL_BUFFER_SIZE = 1024;

  // feature identifiers

  /**
   * Feature identifier: validation.
   */
  protected static final String VALIDATION =
      Constants.SAX_FEATURE_PREFIX + Constants.VALIDATION_FEATURE;

  /**
   * standard uri conformant (strict uri).
   * http://apache.org/xml/features/standard-uri-conformant
   */
  protected boolean fStrictURI;


  /**
   * Feature identifier: external general entities.
   */
  protected static final String EXTERNAL_GENERAL_ENTITIES =
      Constants.SAX_FEATURE_PREFIX + Constants.EXTERNAL_GENERAL_ENTITIES_FEATURE;

  /**
   * Feature identifier: external parameter entities.
   */
  protected static final String EXTERNAL_PARAMETER_ENTITIES =
      Constants.SAX_FEATURE_PREFIX + Constants.EXTERNAL_PARAMETER_ENTITIES_FEATURE;

  /**
   * Feature identifier: allow Java encodings.
   */
  protected static final String ALLOW_JAVA_ENCODINGS =
      Constants.XERCES_FEATURE_PREFIX + Constants.ALLOW_JAVA_ENCODINGS_FEATURE;

  /**
   * Feature identifier: warn on duplicate EntityDef
   */
  protected static final String WARN_ON_DUPLICATE_ENTITYDEF =
      Constants.XERCES_FEATURE_PREFIX + Constants.WARN_ON_DUPLICATE_ENTITYDEF_FEATURE;

  /**
   * Feature identifier: load external DTD.
   */
  protected static final String LOAD_EXTERNAL_DTD =
      Constants.XERCES_FEATURE_PREFIX + Constants.LOAD_EXTERNAL_DTD_FEATURE;

  // property identifiers

  /**
   * Property identifier: symbol table.
   */
  protected static final String SYMBOL_TABLE =
      Constants.XERCES_PROPERTY_PREFIX + Constants.SYMBOL_TABLE_PROPERTY;

  /**
   * Property identifier: error reporter.
   */
  protected static final String ERROR_REPORTER =
      Constants.XERCES_PROPERTY_PREFIX + Constants.ERROR_REPORTER_PROPERTY;

  /**
   * Feature identifier: standard uri conformant
   */
  protected static final String STANDARD_URI_CONFORMANT =
      Constants.XERCES_FEATURE_PREFIX + Constants.STANDARD_URI_CONFORMANT_FEATURE;

  /**
   * Property identifier: entity resolver.
   */
  protected static final String ENTITY_RESOLVER =
      Constants.XERCES_PROPERTY_PREFIX + Constants.ENTITY_RESOLVER_PROPERTY;

  protected static final String STAX_ENTITY_RESOLVER =
      Constants.XERCES_PROPERTY_PREFIX + Constants.STAX_ENTITY_RESOLVER_PROPERTY;

  // property identifier:  ValidationManager
  protected static final String VALIDATION_MANAGER =
      Constants.XERCES_PROPERTY_PREFIX + Constants.VALIDATION_MANAGER_PROPERTY;

  /**
   * property identifier: buffer size.
   */
  protected static final String BUFFER_SIZE =
      Constants.XERCES_PROPERTY_PREFIX + Constants.BUFFER_SIZE_PROPERTY;

  /**
   * property identifier: security manager.
   */
  protected static final String SECURITY_MANAGER =
      Constants.XERCES_PROPERTY_PREFIX + Constants.SECURITY_MANAGER_PROPERTY;

  protected static final String PARSER_SETTINGS =
      Constants.XERCES_FEATURE_PREFIX + Constants.PARSER_SETTINGS;

  /**
   * Property identifier: Security property manager.
   */
  private static final String XML_SECURITY_PROPERTY_MANAGER =
      Constants.XML_SECURITY_PROPERTY_MANAGER;

  /**
   * access external dtd: file protocol
   */
  static final String EXTERNAL_ACCESS_DEFAULT = Constants.EXTERNAL_ACCESS_DEFAULT;

  // recognized features and properties

  /**
   * Recognized features.
   */
  private static final String[] RECOGNIZED_FEATURES = {
      VALIDATION,
      EXTERNAL_GENERAL_ENTITIES,
      EXTERNAL_PARAMETER_ENTITIES,
      ALLOW_JAVA_ENCODINGS,
      WARN_ON_DUPLICATE_ENTITYDEF,
      STANDARD_URI_CONFORMANT
  };

  /**
   * Feature defaults.
   */
  private static final Boolean[] FEATURE_DEFAULTS = {
      null,
      Boolean.TRUE,
      Boolean.TRUE,
      Boolean.TRUE,
      Boolean.FALSE,
      Boolean.FALSE
  };

  /**
   * Recognized properties.
   */
  private static final String[] RECOGNIZED_PROPERTIES = {
      SYMBOL_TABLE,
      ERROR_REPORTER,
      ENTITY_RESOLVER,
      VALIDATION_MANAGER,
      BUFFER_SIZE,
      SECURITY_MANAGER,
      XML_SECURITY_PROPERTY_MANAGER
  };

  /**
   * Property defaults.
   */
  private static final Object[] PROPERTY_DEFAULTS = {
      null,
      null,
      null,
      null,
      new Integer(DEFAULT_BUFFER_SIZE),
      null,
      null
  };

  private static final String XMLEntity = "[xml]".intern();
  private static final String DTDEntity = "[dtd]".intern();

  // debugging

  /**
   * Debug printing of buffer. This debugging flag works best when you
   * resize the DEFAULT_BUFFER_SIZE down to something reasonable like
   * 64 characters.
   */
  private static final boolean DEBUG_BUFFER = false;

  /**
   * warn on duplicate Entity declaration.
   * http://apache.org/xml/features/warn-on-duplicate-entitydef
   */
  protected boolean fWarnDuplicateEntityDef;

  /**
   * Debug some basic entities.
   */
  private static final boolean DEBUG_ENTITIES = false;

  /**
   * Debug switching readers for encodings.
   */
  private static final boolean DEBUG_ENCODINGS = false;

  // should be diplayed trace resolving messages
  private static final boolean DEBUG_RESOLVER = false;

  //
  // Data
  //

  // features

  /**
   * Validation. This feature identifier is:
   * http://xml.org/sax/features/validation
   */
  protected boolean fValidation;

  /**
   * External general entities. This feature identifier is:
   * http://xml.org/sax/features/external-general-entities
   */
  protected boolean fExternalGeneralEntities;

  /**
   * External parameter entities. This feature identifier is:
   * http://xml.org/sax/features/external-parameter-entities
   */
  protected boolean fExternalParameterEntities;

  /**
   * Allow Java encoding names. This feature identifier is:
   * http://apache.org/xml/features/allow-java-encodings
   */
  protected boolean fAllowJavaEncodings = true;

  /**
   * Load external DTD.
   */
  protected boolean fLoadExternalDTD = true;

  // properties

  /**
   * Symbol table. This property identifier is:
   * http://apache.org/xml/properties/internal/symbol-table
   */
  protected SymbolTable fSymbolTable;

  /**
   * Error reporter. This property identifier is:
   * http://apache.org/xml/properties/internal/error-reporter
   */
  protected XMLErrorReporter fErrorReporter;

  /**
   * Entity resolver. This property identifier is:
   * http://apache.org/xml/properties/internal/entity-resolver
   */
  protected XMLEntityResolver fEntityResolver;

  /**
   * Stax Entity Resolver. This property identifier is XMLInputFactory.ENTITY_RESOLVER
   */

  protected StaxEntityResolverWrapper fStaxEntityResolver;

  /**
   * Property Manager. This is used from Stax
   */
  protected PropertyManager fPropertyManager;

  /**
   * StAX properties
   */
  boolean fSupportDTD = true;
  boolean fReplaceEntityReferences = true;
  boolean fSupportExternalEntities = true;

  /**
   * used to restrict external access
   */
  protected String fAccessExternalDTD = EXTERNAL_ACCESS_DEFAULT;

  // settings

  /**
   * Validation manager. This property identifier is:
   * http://apache.org/xml/properties/internal/validation-manager
   */
  protected ValidationManager fValidationManager;

  // settings

  /**
   * Buffer size. We get this value from a property. The default size
   * is used if the input buffer size property is not specified.
   * REVISIT: do we need a property for internal entity buffer size?
   */
  protected int fBufferSize = DEFAULT_BUFFER_SIZE;

  /**
   * Security Manager
   */
  protected XMLSecurityManager fSecurityManager = null;

  protected XMLLimitAnalyzer fLimitAnalyzer = null;

  protected int entityExpansionIndex;

  /**
   * True if the document entity is standalone. This should really
   * only be set by the document source (e.g. XMLDocumentScanner).
   */
  protected boolean fStandalone;

  // are the entities being parsed in the external subset?
  // NOTE:  this *is not* the same as whether they're external entities!
  protected boolean fInExternalSubset = false;

  // handlers
  /**
   * Entity handler.
   */
  protected XMLEntityHandler fEntityHandler;

  /**
   * Current entity scanner
   */
  protected XMLEntityScanner fEntityScanner;

  /**
   * XML 1.0 entity scanner.
   */
  protected XMLEntityScanner fXML10EntityScanner;

  /**
   * XML 1.1 entity scanner.
   */
  protected XMLEntityScanner fXML11EntityScanner;

  /**
   * count of entities expanded:
   */
  protected int fEntityExpansionCount = 0;

  // entities

  /**
   * Entities.
   */
  protected Hashtable fEntities = new Hashtable();

  /**
   * Entity stack.
   */
  protected Stack fEntityStack = new Stack();

  /**
   * Current entity.
   */
  protected Entity.ScannedEntity fCurrentEntity = null;

  /**
   * identify if the InputSource is created by a resolver
   */
  boolean fISCreatedByResolver = false;

  // shared context

  protected XMLEntityStorage fEntityStorage;

  protected final Object[] defaultEncoding = new Object[]{"UTF-8", null};

  // temp vars

  /**
   * Resource identifer.
   */
  private final XMLResourceIdentifierImpl fResourceIdentifier = new XMLResourceIdentifierImpl();

  /**
   * Augmentations for entities.
   */
  private final Augmentations fEntityAugs = new AugmentationsImpl();

  /**
   * Pool of character buffers.
   */
  private CharacterBufferPool fBufferPool = new CharacterBufferPool(fBufferSize,
      DEFAULT_INTERNAL_BUFFER_SIZE);

  //
  // Constructors
  //

  /**
   * If this constructor is used to create the object, reset() should be invoked on this object
   */
  public XMLEntityManager() {
    fEntityStorage = new XMLEntityStorage(this);
    setScannerVersion(Constants.XML_VERSION_1_0);
  } // <init>()

  /**
   * Default constructor.
   */
  public XMLEntityManager(PropertyManager propertyManager) {
    fPropertyManager = propertyManager;
    //pass a reference to current entity being scanned
    //fEntityStorage = new XMLEntityStorage(fCurrentEntity) ;
    fEntityStorage = new XMLEntityStorage(this);
    fEntityScanner = new XMLEntityScanner(propertyManager, this);
    reset(propertyManager);
  } // <init>()

  /**
   * Adds an internal entity declaration.
   * <p>
   * <strong>Note:</strong> This method ignores subsequent entity
   * declarations.
   * <p>
   * <strong>Note:</strong> The name should be a unique symbol. The
   * SymbolTable can be used for this purpose.
   *
   * @param name The name of the entity.
   * @param text The text of the entity.
   * @see SymbolTable
   */
  public void addInternalEntity(String name, String text) {
    if (!fEntities.containsKey(name)) {
      Entity entity = new Entity.InternalEntity(name, text, fInExternalSubset);
      fEntities.put(name, entity);
    } else {
      if (fWarnDuplicateEntityDef) {
        fErrorReporter.reportError(XMLMessageFormatter.XML_DOMAIN,
            "MSG_DUPLICATE_ENTITY_DEFINITION",
            new Object[]{name},
            XMLErrorReporter.SEVERITY_WARNING);
      }
    }

  } // addInternalEntity(String,String)

  /**
   * Adds an external entity declaration.
   * <p>
   * <strong>Note:</strong> This method ignores subsequent entity
   * declarations.
   * <p>
   * <strong>Note:</strong> The name should be a unique symbol. The
   * SymbolTable can be used for this purpose.
   *
   * @param name The name of the entity.
   * @param publicId The public identifier of the entity.
   * @param literalSystemId The system identifier of the entity.
   * @param baseSystemId The base system identifier of the entity. This is the system identifier of
   * the entity where <em>the entity being added</em> and is used to expand the system identifier
   * when the system identifier is a relative URI. When null the system identifier of the first
   * external entity on the stack is used instead.
   * @see SymbolTable
   */
  public void addExternalEntity(String name,
      String publicId, String literalSystemId,
      String baseSystemId) throws IOException {
    if (!fEntities.containsKey(name)) {
      if (baseSystemId == null) {
        // search for the first external entity on the stack
        int size = fEntityStack.size();
        if (size == 0 && fCurrentEntity != null && fCurrentEntity.entityLocation != null) {
          baseSystemId = fCurrentEntity.entityLocation.getExpandedSystemId();
        }
        for (int i = size - 1; i >= 0; i--) {
          Entity.ScannedEntity externalEntity =
              (Entity.ScannedEntity) fEntityStack.elementAt(i);
          if (externalEntity.entityLocation != null
              && externalEntity.entityLocation.getExpandedSystemId() != null) {
            baseSystemId = externalEntity.entityLocation.getExpandedSystemId();
            break;
          }
        }
      }
      Entity entity = new Entity.ExternalEntity(name,
          new XMLEntityDescriptionImpl(name, publicId, literalSystemId, baseSystemId,
              expandSystemId(literalSystemId, baseSystemId, false)), null, fInExternalSubset);
      fEntities.put(name, entity);
    } else {
      if (fWarnDuplicateEntityDef) {
        fErrorReporter.reportError(XMLMessageFormatter.XML_DOMAIN,
            "MSG_DUPLICATE_ENTITY_DEFINITION",
            new Object[]{name},
            XMLErrorReporter.SEVERITY_WARNING);
      }
    }

  } // addExternalEntity(String,String,String,String)


  /**
   * Adds an unparsed entity declaration.
   * <p>
   * <strong>Note:</strong> This method ignores subsequent entity
   * declarations.
   * <p>
   * <strong>Note:</strong> The name should be a unique symbol. The
   * SymbolTable can be used for this purpose.
   *
   * @param name The name of the entity.
   * @param publicId The public identifier of the entity.
   * @param systemId The system identifier of the entity.
   * @param notation The name of the notation.
   * @see SymbolTable
   */
  public void addUnparsedEntity(String name,
      String publicId, String systemId,
      String baseSystemId, String notation) {
    if (!fEntities.containsKey(name)) {
      Entity.ExternalEntity entity = new Entity.ExternalEntity(name,
          new XMLEntityDescriptionImpl(name, publicId, systemId, baseSystemId, null),
          notation, fInExternalSubset);
      fEntities.put(name, entity);
    } else {
      if (fWarnDuplicateEntityDef) {
        fErrorReporter.reportError(XMLMessageFormatter.XML_DOMAIN,
            "MSG_DUPLICATE_ENTITY_DEFINITION",
            new Object[]{name},
            XMLErrorReporter.SEVERITY_WARNING);
      }
    }
  } // addUnparsedEntity(String,String,String,String)


  /**
   * get the entity storage object from entity manager
   */
  public XMLEntityStorage getEntityStore() {
    return fEntityStorage;
  }

  /**
   * return the entity responsible for reading the entity
   */
  public XMLEntityScanner getEntityScanner() {
    if (fEntityScanner == null) {
      // default to 1.0
      if (fXML10EntityScanner == null) {
        fXML10EntityScanner = new XMLEntityScanner();
      }
      fXML10EntityScanner.reset(fSymbolTable, this, fErrorReporter);
      fEntityScanner = fXML10EntityScanner;
    }
    return fEntityScanner;

  }

  public void setScannerVersion(short version) {

    if (version == Constants.XML_VERSION_1_0) {
      if (fXML10EntityScanner == null) {
        fXML10EntityScanner = new XMLEntityScanner();
      }
      fXML10EntityScanner.reset(fSymbolTable, this, fErrorReporter);
      fEntityScanner = fXML10EntityScanner;
      fEntityScanner.setCurrentEntity(fCurrentEntity);
    } else {
      if (fXML11EntityScanner == null) {
        fXML11EntityScanner = new XML11EntityScanner();
      }
      fXML11EntityScanner.reset(fSymbolTable, this, fErrorReporter);
      fEntityScanner = fXML11EntityScanner;
      fEntityScanner.setCurrentEntity(fCurrentEntity);
    }

  }

  /**
   * This method uses the passed-in XMLInputSource to make
   * fCurrentEntity usable for reading.
   *
   * @param name name of the entity (XML is it's the document entity)
   * @param xmlInputSource the input source, with sufficient information to begin scanning
   * characters.
   * @param literal True if this entity is started within a literal value.
   * @param isExternal whether this entity should be treated as an internal or external entity.
   * @return the encoding of the new entity or null if a character stream was employed
   * @throws IOException if anything can't be read XNIException    If any parser-specific goes
   * wrong.
   */
  public String setupCurrentEntity(String name, XMLInputSource xmlInputSource,
      boolean literal, boolean isExternal)
      throws IOException, XNIException {
    // get information

    final String publicId = xmlInputSource.getPublicId();
    String literalSystemId = xmlInputSource.getSystemId();
    String baseSystemId = xmlInputSource.getBaseSystemId();
    String encoding = xmlInputSource.getEncoding();
    final boolean encodingExternallySpecified = (encoding != null);
    Boolean isBigEndian = null;

    // create reader
    InputStream stream = null;
    Reader reader = xmlInputSource.getCharacterStream();

    // First chance checking strict URI
    String expandedSystemId = expandSystemId(literalSystemId, baseSystemId, fStrictURI);
    if (baseSystemId == null) {
      baseSystemId = expandedSystemId;
    }
    if (reader == null) {
      stream = xmlInputSource.getByteStream();
      if (stream == null) {
        URL location = new URL(expandedSystemId);
        URLConnection connect = location.openConnection();
        if (!(connect instanceof HttpURLConnection)) {
          stream = connect.getInputStream();
        } else {
          boolean followRedirects = true;

          // setup URLConnection if we have an HTTPInputSource
          if (xmlInputSource instanceof HTTPInputSource) {
            final HttpURLConnection urlConnection = (HttpURLConnection) connect;
            final HTTPInputSource httpInputSource = (HTTPInputSource) xmlInputSource;

            // set request properties
            Iterator propIter = httpInputSource.getHTTPRequestProperties();
            while (propIter.hasNext()) {
              Map.Entry entry = (Map.Entry) propIter.next();
              urlConnection.setRequestProperty((String) entry.getKey(), (String) entry.getValue());
            }

            // set preference for redirection
            followRedirects = httpInputSource.getFollowHTTPRedirects();
            if (!followRedirects) {
              setInstanceFollowRedirects(urlConnection, followRedirects);
            }
          }

          stream = connect.getInputStream();

          // REVISIT: If the URLConnection has external encoding
          // information, we should be reading it here. It's located
          // in the charset parameter of Content-Type. -- mrglavas

          if (followRedirects) {
            String redirect = connect.getURL().toString();
            // E43: Check if the URL was redirected, and then
            // update literal and expanded system IDs if needed.
            if (!redirect.equals(expandedSystemId)) {
              literalSystemId = redirect;
              expandedSystemId = redirect;
            }
          }
        }
      }

      // wrap this stream in RewindableInputStream
      stream = new RewindableInputStream(stream);

      // perform auto-detect of encoding if necessary
      if (encoding == null) {
        // read first four bytes and determine encoding
        final byte[] b4 = new byte[4];
        int count = 0;
        for (; count < 4; count++) {
          b4[count] = (byte) stream.read();
        }
        if (count == 4) {
          Object[] encodingDesc = getEncodingName(b4, count);
          encoding = (String) (encodingDesc[0]);
          isBigEndian = (Boolean) (encodingDesc[1]);

          stream.reset();
          // Special case UTF-8 files with BOM created by Microsoft
          // tools. It's more efficient to consume the BOM than make
          // the reader perform extra checks. -Ac
          if (count > 2 && encoding.equals("UTF-8")) {
            int b0 = b4[0] & 0xFF;
            int b1 = b4[1] & 0xFF;
            int b2 = b4[2] & 0xFF;
            if (b0 == 0xEF && b1 == 0xBB && b2 == 0xBF) {
              // ignore first three bytes...
              stream.skip(3);
            }
          }
          reader = createReader(stream, encoding, isBigEndian);
        } else {
          reader = createReader(stream, encoding, isBigEndian);
        }
      }

      // use specified encoding
      else {
        encoding = encoding.toUpperCase(Locale.ENGLISH);

        // If encoding is UTF-8, consume BOM if one is present.
        if (encoding.equals("UTF-8")) {
          final int[] b3 = new int[3];
          int count = 0;
          for (; count < 3; ++count) {
            b3[count] = stream.read();
            if (b3[count] == -1) {
              break;
            }
          }
          if (count == 3) {
            if (b3[0] != 0xEF || b3[1] != 0xBB || b3[2] != 0xBF) {
              // First three bytes are not BOM, so reset.
              stream.reset();
            }
          } else {
            stream.reset();
          }
        }
        // If encoding is UTF-16, we still need to read the first four bytes
        // in order to discover the byte order.
        else if (encoding.equals("UTF-16")) {
          final int[] b4 = new int[4];
          int count = 0;
          for (; count < 4; ++count) {
            b4[count] = stream.read();
            if (b4[count] == -1) {
              break;
            }
          }
          stream.reset();

          String utf16Encoding = "UTF-16";
          if (count >= 2) {
            final int b0 = b4[0];
            final int b1 = b4[1];
            if (b0 == 0xFE && b1 == 0xFF) {
              // UTF-16, big-endian
              utf16Encoding = "UTF-16BE";
              isBigEndian = Boolean.TRUE;
            } else if (b0 == 0xFF && b1 == 0xFE) {
              // UTF-16, little-endian
              utf16Encoding = "UTF-16LE";
              isBigEndian = Boolean.FALSE;
            } else if (count == 4) {
              final int b2 = b4[2];
              final int b3 = b4[3];
              if (b0 == 0x00 && b1 == 0x3C && b2 == 0x00 && b3 == 0x3F) {
                // UTF-16, big-endian, no BOM
                utf16Encoding = "UTF-16BE";
                isBigEndian = Boolean.TRUE;
              }
              if (b0 == 0x3C && b1 == 0x00 && b2 == 0x3F && b3 == 0x00) {
                // UTF-16, little-endian, no BOM
                utf16Encoding = "UTF-16LE";
                isBigEndian = Boolean.FALSE;
              }
            }
          }
          reader = createReader(stream, utf16Encoding, isBigEndian);
        }
        // If encoding is UCS-4, we still need to read the first four bytes
        // in order to discover the byte order.
        else if (encoding.equals("ISO-10646-UCS-4")) {
          final int[] b4 = new int[4];
          int count = 0;
          for (; count < 4; ++count) {
            b4[count] = stream.read();
            if (b4[count] == -1) {
              break;
            }
          }
          stream.reset();

          // Ignore unusual octet order for now.
          if (count == 4) {
            // UCS-4, big endian (1234)
            if (b4[0] == 0x00 && b4[1] == 0x00 && b4[2] == 0x00 && b4[3] == 0x3C) {
              isBigEndian = Boolean.TRUE;
            }
            // UCS-4, little endian (1234)
            else if (b4[0] == 0x3C && b4[1] == 0x00 && b4[2] == 0x00 && b4[3] == 0x00) {
              isBigEndian = Boolean.FALSE;
            }
          }
        }
        // If encoding is UCS-2, we still need to read the first four bytes
        // in order to discover the byte order.
        else if (encoding.equals("ISO-10646-UCS-2")) {
          final int[] b4 = new int[4];
          int count = 0;
          for (; count < 4; ++count) {
            b4[count] = stream.read();
            if (b4[count] == -1) {
              break;
            }
          }
          stream.reset();

          if (count == 4) {
            // UCS-2, big endian
            if (b4[0] == 0x00 && b4[1] == 0x3C && b4[2] == 0x00 && b4[3] == 0x3F) {
              isBigEndian = Boolean.TRUE;
            }
            // UCS-2, little endian
            else if (b4[0] == 0x3C && b4[1] == 0x00 && b4[2] == 0x3F && b4[3] == 0x00) {
              isBigEndian = Boolean.FALSE;
            }
          }
        }

        reader = createReader(stream, encoding, isBigEndian);
      }

      // read one character at a time so we don't jump too far
      // ahead, converting characters from the byte stream in
      // the wrong encoding
      if (DEBUG_ENCODINGS) {
        System.out.println("$$$ no longer wrapping reader in OneCharReader");
      }
      //reader = new OneCharReader(reader);
    }

    // We've seen a new Reader.
    // Push it on the stack so we can close it later.
    //fOwnReaders.add(reader);

    // push entity on stack
    if (fCurrentEntity != null) {
      fEntityStack.push(fCurrentEntity);
    }

    // create entity
        /* if encoding is specified externally, 'encoding' information present
         * in the prolog of the XML document is not considered. Hence, prolog can
         * be read in Chunks of data instead of byte by byte.
         */
    fCurrentEntity = new com.sun.xml.internal.stream.Entity.ScannedEntity(name,
        new XMLResourceIdentifierImpl(publicId, literalSystemId, baseSystemId, expandedSystemId),
        stream, reader, encoding, literal, encodingExternallySpecified, isExternal);
    fCurrentEntity.setEncodingExternallySpecified(encodingExternallySpecified);
    fEntityScanner.setCurrentEntity(fCurrentEntity);
    fResourceIdentifier.setValues(publicId, literalSystemId, baseSystemId, expandedSystemId);
    if (fLimitAnalyzer != null) {
      fLimitAnalyzer.startEntity(name);
    }
    return encoding;
  } //setupCurrentEntity(String, XMLInputSource, boolean, boolean):  String


  /**
   * Checks whether an entity given by name is external.
   *
   * @param entityName The name of the entity to check.
   * @return True if the entity is external, false otherwise (including when the entity is not
   * declared).
   */
  public boolean isExternalEntity(String entityName) {

    Entity entity = (Entity) fEntities.get(entityName);
    if (entity == null) {
      return false;
    }
    return entity.isExternal();
  }

  /**
   * Checks whether the declaration of an entity given by name is
   * // in the external subset.
   *
   * @param entityName The name of the entity to check.
   * @return True if the entity was declared in the external subset, false otherwise (including when
   * the entity is not declared).
   */
  public boolean isEntityDeclInExternalSubset(String entityName) {

    Entity entity = (Entity) fEntities.get(entityName);
    if (entity == null) {
      return false;
    }
    return entity.isEntityDeclInExternalSubset();
  }

  //
  // Public methods
  //

  /**
   * Sets whether the document entity is standalone.
   *
   * @param standalone True if document entity is standalone.
   */
  public void setStandalone(boolean standalone) {
    fStandalone = standalone;
  }
  // setStandalone(boolean)

  /**
   * Returns true if the document entity is standalone.
   */
  public boolean isStandalone() {
    return fStandalone;
  }  //isStandalone():boolean

  public boolean isDeclaredEntity(String entityName) {

    Entity entity = (Entity) fEntities.get(entityName);
    return entity != null;
  }

  public boolean isUnparsedEntity(String entityName) {

    Entity entity = (Entity) fEntities.get(entityName);
    if (entity == null) {
      return false;
    }
    return entity.isUnparsed();
  }


  // this simply returns the fResourceIdentifier object;
  // this should only be used with caution by callers that
  // carefully manage the entity manager's behaviour, so that
  // this doesn't returning meaningless or misleading data.
  // @return  a reference to the current fResourceIdentifier object
  public XMLResourceIdentifier getCurrentResourceIdentifier() {
    return fResourceIdentifier;
  }

  /**
   * Sets the entity handler. When an entity starts and ends, the
   * entity handler is notified of the change.
   *
   * @param entityHandler The new entity handler.
   */

  public void setEntityHandler(
      com.sun.org.apache.xerces.internal.impl.XMLEntityHandler entityHandler) {
    fEntityHandler = (XMLEntityHandler) entityHandler;
  } // setEntityHandler(XMLEntityHandler)

  //this function returns StaxXMLInputSource
  public StaxXMLInputSource resolveEntityAsPerStax(XMLResourceIdentifier resourceIdentifier)
      throws java.io.IOException {

    if (resourceIdentifier == null) {
      return null;
    }

    String publicId = resourceIdentifier.getPublicId();
    String literalSystemId = resourceIdentifier.getLiteralSystemId();
    String baseSystemId = resourceIdentifier.getBaseSystemId();
    String expandedSystemId = resourceIdentifier.getExpandedSystemId();
    // if no base systemId given, assume that it's relative
    // to the systemId of the current scanned entity
    // Sometimes the system id is not (properly) expanded.
    // We need to expand the system id if:
    // a. the expanded one was null; or
    // b. the base system id was null, but becomes non-null from the current entity.
    boolean needExpand = (expandedSystemId == null);
    // REVISIT:  why would the baseSystemId ever be null?  if we
    // didn't have to make this check we wouldn't have to reuse the
    // fXMLResourceIdentifier object...
    if (baseSystemId == null && fCurrentEntity != null && fCurrentEntity.entityLocation != null) {
      baseSystemId = fCurrentEntity.entityLocation.getExpandedSystemId();
      if (baseSystemId != null) {
        needExpand = true;
      }
    }
    if (needExpand) {
      expandedSystemId = expandSystemId(literalSystemId, baseSystemId, false);
    }

    // give the entity resolver a chance
    StaxXMLInputSource staxInputSource = null;
    XMLInputSource xmlInputSource = null;

    XMLResourceIdentifierImpl ri = null;

    if (resourceIdentifier instanceof XMLResourceIdentifierImpl) {
      ri = (XMLResourceIdentifierImpl) resourceIdentifier;
    } else {
      fResourceIdentifier.clear();
      ri = fResourceIdentifier;
    }
    ri.setValues(publicId, literalSystemId, baseSystemId, expandedSystemId);
    if (DEBUG_RESOLVER) {
      System.out.println("BEFORE Calling resolveEntity");
    }

    fISCreatedByResolver = false;
    //either of Stax or Xerces would be null
    if (fStaxEntityResolver != null) {
      staxInputSource = fStaxEntityResolver.resolveEntity(ri);
      if (staxInputSource != null) {
        fISCreatedByResolver = true;
      }
    }

    if (fEntityResolver != null) {
      xmlInputSource = fEntityResolver.resolveEntity(ri);
      if (xmlInputSource != null) {
        fISCreatedByResolver = true;
      }
    }

    if (xmlInputSource != null) {
      //wrap this XMLInputSource to StaxInputSource
      staxInputSource = new StaxXMLInputSource(xmlInputSource, fISCreatedByResolver);
    }

    // do default resolution
    //this works for both stax & Xerces, if staxInputSource is null, it means parser need to revert to default resolution
    if (staxInputSource == null) {
      // REVISIT: when systemId is null, I think we should return null.
      //          is this the right solution? -SG
      //if (systemId != null)
      staxInputSource = new StaxXMLInputSource(
          new XMLInputSource(publicId, literalSystemId, baseSystemId));
    } else if (staxInputSource.hasXMLStreamOrXMLEventReader()) {
      //Waiting for the clarification from EG. - nb
    }

    if (DEBUG_RESOLVER) {
      System.err.println("XMLEntityManager.resolveEntity(" + publicId + ")");
      System.err.println(" = " + xmlInputSource);
    }

    return staxInputSource;

  }

  /**
   * Resolves the specified public and system identifiers. This
   * method first attempts to resolve the entity based on the
   * EntityResolver registered by the application. If no entity
   * resolver is registered or if the registered entity handler
   * is unable to resolve the entity, then default entity
   * resolution will occur.
   *
   * @param publicId The public identifier of the entity.
   * @param systemId The system identifier of the entity.
   * @param baseSystemId The base system identifier of the entity. This is the system identifier of
   * the current entity and is used to expand the system identifier when the system identifier is a
   * relative URI.
   * @return Returns an input source that wraps the resolved entity. This method will never return
   * null.
   * @throws IOException Thrown on i/o error.
   * @throws XNIException Thrown by entity resolver to signal an error.
   */
  public XMLInputSource resolveEntity(XMLResourceIdentifier resourceIdentifier)
      throws IOException, XNIException {
    if (resourceIdentifier == null) {
      return null;
    }
    String publicId = resourceIdentifier.getPublicId();
    String literalSystemId = resourceIdentifier.getLiteralSystemId();
    String baseSystemId = resourceIdentifier.getBaseSystemId();
    String expandedSystemId = resourceIdentifier.getExpandedSystemId();
    String namespace = resourceIdentifier.getNamespace();

    // if no base systemId given, assume that it's relative
    // to the systemId of the current scanned entity
    // Sometimes the system id is not (properly) expanded.
    // We need to expand the system id if:
    // a. the expanded one was null; or
    // b. the base system id was null, but becomes non-null from the current entity.
    boolean needExpand = (expandedSystemId == null);
    // REVISIT:  why would the baseSystemId ever be null?  if we
    // didn't have to make this check we wouldn't have to reuse the
    // fXMLResourceIdentifier object...
    if (baseSystemId == null && fCurrentEntity != null && fCurrentEntity.entityLocation != null) {
      baseSystemId = fCurrentEntity.entityLocation.getExpandedSystemId();
      if (baseSystemId != null) {
        needExpand = true;
      }
    }
    if (needExpand) {
      expandedSystemId = expandSystemId(literalSystemId, baseSystemId, false);
    }

    // give the entity resolver a chance
    XMLInputSource xmlInputSource = null;

    if (fEntityResolver != null) {
      resourceIdentifier.setBaseSystemId(baseSystemId);
      resourceIdentifier.setExpandedSystemId(expandedSystemId);
      xmlInputSource = fEntityResolver.resolveEntity(resourceIdentifier);
    }

    // do default resolution
    // REVISIT: what's the correct behavior if the user provided an entity
    // resolver (fEntityResolver != null), but resolveEntity doesn't return
    // an input source (xmlInputSource == null)?
    // do we do default resolution, or do we just return null? -SG
    if (xmlInputSource == null) {
      // REVISIT: when systemId is null, I think we should return null.
      //          is this the right solution? -SG
      //if (systemId != null)
      xmlInputSource = new XMLInputSource(publicId, literalSystemId, baseSystemId);
    }

    if (DEBUG_RESOLVER) {
      System.err.println("XMLEntityManager.resolveEntity(" + publicId + ")");
      System.err.println(" = " + xmlInputSource);
    }

    return xmlInputSource;

  } // resolveEntity(XMLResourceIdentifier):XMLInputSource

  /**
   * Starts a named entity.
   *
   * @param entityName The name of the entity to start.
   * @param literal True if this entity is started within a literal value.
   * @throws IOException Thrown on i/o error.
   * @throws XNIException Thrown by entity handler to signal an error.
   */
  public void startEntity(String entityName, boolean literal)
      throws IOException, XNIException {

    // was entity declared?
    Entity entity = (Entity) fEntityStorage.getEntity(entityName);
    if (entity == null) {
      if (fEntityHandler != null) {
        String encoding = null;
        fResourceIdentifier.clear();
        fEntityAugs.removeAllItems();
        fEntityAugs.putItem(Constants.ENTITY_SKIPPED, Boolean.TRUE);
        fEntityHandler.startEntity(entityName, fResourceIdentifier, encoding, fEntityAugs);
        fEntityAugs.removeAllItems();
        fEntityAugs.putItem(Constants.ENTITY_SKIPPED, Boolean.TRUE);
        fEntityHandler.endEntity(entityName, fEntityAugs);
      }
      return;
    }

    // should we skip external entities?
    boolean external = entity.isExternal();
    Entity.ExternalEntity externalEntity = null;
    String extLitSysId = null, extBaseSysId = null, expandedSystemId = null;
    if (external) {
      externalEntity = (Entity.ExternalEntity) entity;
      extLitSysId = (externalEntity.entityLocation != null ? externalEntity.entityLocation
          .getLiteralSystemId() : null);
      extBaseSysId = (externalEntity.entityLocation != null ? externalEntity.entityLocation
          .getBaseSystemId() : null);
      expandedSystemId = expandSystemId(extLitSysId, extBaseSysId);
      boolean unparsed = entity.isUnparsed();
      boolean parameter = entityName.startsWith("%");
      boolean general = !parameter;
      if (unparsed || (general && !fExternalGeneralEntities) ||
          (parameter && !fExternalParameterEntities) ||
          !fSupportDTD || !fSupportExternalEntities) {

        if (fEntityHandler != null) {
          fResourceIdentifier.clear();
          final String encoding = null;
          fResourceIdentifier.setValues(
              (externalEntity.entityLocation != null ? externalEntity.entityLocation.getPublicId()
                  : null),
              extLitSysId, extBaseSysId, expandedSystemId);
          fEntityAugs.removeAllItems();
          fEntityAugs.putItem(Constants.ENTITY_SKIPPED, Boolean.TRUE);
          fEntityHandler.startEntity(entityName, fResourceIdentifier, encoding, fEntityAugs);
          fEntityAugs.removeAllItems();
          fEntityAugs.putItem(Constants.ENTITY_SKIPPED, Boolean.TRUE);
          fEntityHandler.endEntity(entityName, fEntityAugs);
        }
        return;
      }
    }

    // is entity recursive?
    int size = fEntityStack.size();
    for (int i = size; i >= 0; i--) {
      Entity activeEntity = i == size
          ? fCurrentEntity
          : (Entity) fEntityStack.elementAt(i);
      if (activeEntity.name == entityName) {
        String path = entityName;
        for (int j = i + 1; j < size; j++) {
          activeEntity = (Entity) fEntityStack.elementAt(j);
          path = path + " -> " + activeEntity.name;
        }
        path = path + " -> " + fCurrentEntity.name;
        path = path + " -> " + entityName;
        fErrorReporter.reportError(this.getEntityScanner(), XMLMessageFormatter.XML_DOMAIN,
            "RecursiveReference",
            new Object[]{entityName, path},
            XMLErrorReporter.SEVERITY_FATAL_ERROR);

        if (fEntityHandler != null) {
          fResourceIdentifier.clear();
          final String encoding = null;
          if (external) {
            fResourceIdentifier.setValues(
                (externalEntity.entityLocation != null ? externalEntity.entityLocation.getPublicId()
                    : null),
                extLitSysId, extBaseSysId, expandedSystemId);
          }
          fEntityAugs.removeAllItems();
          fEntityAugs.putItem(Constants.ENTITY_SKIPPED, Boolean.TRUE);
          fEntityHandler.startEntity(entityName, fResourceIdentifier, encoding, fEntityAugs);
          fEntityAugs.removeAllItems();
          fEntityAugs.putItem(Constants.ENTITY_SKIPPED, Boolean.TRUE);
          fEntityHandler.endEntity(entityName, fEntityAugs);
        }

        return;
      }
    }

    // resolve external entity
    StaxXMLInputSource staxInputSource = null;
    XMLInputSource xmlInputSource = null;

    if (external) {
      staxInputSource = resolveEntityAsPerStax(externalEntity.entityLocation);
      /** xxx:  Waiting from the EG
       * //simply return if there was entity resolver registered and application
       * //returns either XMLStreamReader or XMLEventReader.
       * if(staxInputSource.hasXMLStreamOrXMLEventReader()) return ;
       */
      xmlInputSource = staxInputSource.getXMLInputSource();
      if (!fISCreatedByResolver) {
        //let the not-LoadExternalDTD or not-SupportDTD process to handle the situation
        if (fLoadExternalDTD) {
          String accessError = SecuritySupport
              .checkAccess(expandedSystemId, fAccessExternalDTD, Constants.ACCESS_EXTERNAL_ALL);
          if (accessError != null) {
            fErrorReporter.reportError(this.getEntityScanner(), XMLMessageFormatter.XML_DOMAIN,
                "AccessExternalEntity",
                new Object[]{SecuritySupport.sanitizePath(expandedSystemId), accessError},
                XMLErrorReporter.SEVERITY_FATAL_ERROR);
          }
        }
      }
    }
    // wrap internal entity
    else {
      Entity.InternalEntity internalEntity = (Entity.InternalEntity) entity;
      Reader reader = new StringReader(internalEntity.text);
      xmlInputSource = new XMLInputSource(null, null, null, reader, null);
    }

    // start the entity
    startEntity(entityName, xmlInputSource, literal, external);

  } // startEntity(String,boolean)

  /**
   * Starts the document entity. The document entity has the "[xml]"
   * pseudo-name.
   *
   * @param xmlInputSource The input source of the document entity.
   * @throws IOException Thrown on i/o error.
   * @throws XNIException Thrown by entity handler to signal an error.
   */
  public void startDocumentEntity(XMLInputSource xmlInputSource)
      throws IOException, XNIException {
    startEntity(XMLEntity, xmlInputSource, false, true);
  } // startDocumentEntity(XMLInputSource)

  //xxx these methods are not required.

  /**
   * Starts the DTD entity. The DTD entity has the "[dtd]"
   * pseudo-name.
   *
   * @param xmlInputSource The input source of the DTD entity.
   * @throws IOException Thrown on i/o error.
   * @throws XNIException Thrown by entity handler to signal an error.
   */
  public void startDTDEntity(XMLInputSource xmlInputSource)
      throws IOException, XNIException {
    startEntity(DTDEntity, xmlInputSource, false, true);
  } // startDTDEntity(XMLInputSource)

  // indicate start of external subset so that
  // location of entity decls can be tracked
  public void startExternalSubset() {
    fInExternalSubset = true;
  }

  public void endExternalSubset() {
    fInExternalSubset = false;
  }

  /**
   * Starts an entity.
   * <p>
   * This method can be used to insert an application defined XML
   * entity stream into the parsing stream.
   *
   * @param name The name of the entity.
   * @param xmlInputSource The input source of the entity.
   * @param literal True if this entity is started within a literal value.
   * @param isExternal whether this entity should be treated as an internal or external entity.
   * @throws IOException Thrown on i/o error.
   * @throws XNIException Thrown by entity handler to signal an error.
   */
  public void startEntity(String name,
      XMLInputSource xmlInputSource,
      boolean literal, boolean isExternal)
      throws IOException, XNIException {

    String encoding = setupCurrentEntity(name, xmlInputSource, literal, isExternal);

    //when entity expansion limit is set by the Application, we need to
    //check for the entity expansion limit set by the parser, if number of entity
    //expansions exceeds the entity expansion limit, parser will throw fatal error.
    // Note that this represents the nesting level of open entities.
    fEntityExpansionCount++;
    if (fLimitAnalyzer != null) {
      fLimitAnalyzer.addValue(entityExpansionIndex, name, 1);
    }
    if (fSecurityManager != null && fSecurityManager
        .isOverLimit(entityExpansionIndex, fLimitAnalyzer)) {
      fSecurityManager.debugPrint(fLimitAnalyzer);
      fErrorReporter.reportError(XMLMessageFormatter.XML_DOMAIN, "EntityExpansionLimitExceeded",
          new Object[]{fSecurityManager.getLimitValueByIndex(entityExpansionIndex)},
          XMLErrorReporter.SEVERITY_FATAL_ERROR);
      // is there anything better to do than reset the counter?
      // at least one can envision debugging applications where this might
      // be useful...
      fEntityExpansionCount = 0;
    }

    // call handler
    if (fEntityHandler != null) {
      fEntityHandler.startEntity(name, fResourceIdentifier, encoding, null);
    }

  } // startEntity(String,XMLInputSource)

  /**
   * Return the current entity being scanned. Current entity is SET using startEntity function.
   *
   * @return Entity.ScannedEntity
   */

  public Entity.ScannedEntity getCurrentEntity() {
    return fCurrentEntity;
  }

  /**
   * Return the top level entity handled by this manager, or null
   * if no entity was added.
   */
  public Entity.ScannedEntity getTopLevelEntity() {
    return (Entity.ScannedEntity)
        (fEntityStack.empty() ? null : fEntityStack.elementAt(0));
  }


  /**
   * Close all opened InputStreams and Readers opened by this parser.
   */
  public void closeReaders() {
    /** this call actually does nothing, readers are closed in the endEntity method
     * through the current entity.
     * The change seems to have happened during the jdk6 development with the
     * addition of StAX
     **/
  }

  public void endEntity() throws IOException, XNIException {

    // call handler
    if (DEBUG_BUFFER) {
      System.out.print("(endEntity: ");
      print();
      System.out.println();
    }
    //pop the entity from the stack
    Entity.ScannedEntity entity =
        fEntityStack.size() > 0 ? (Entity.ScannedEntity) fEntityStack.pop() : null;

    /** need to close the reader first since the program can end
     *  prematurely (e.g. fEntityHandler.endEntity may throw exception)
     *  leaving the reader open
     */
    //close the reader
    if (fCurrentEntity != null) {
      //close the reader
      try {
        if (fLimitAnalyzer != null) {
          fLimitAnalyzer
              .endEntity(XMLSecurityManager.Limit.GENERAL_ENTITY_SIZE_LIMIT, fCurrentEntity.name);
          if (fCurrentEntity.name.equals("[xml]")) {
            fSecurityManager.debugPrint(fLimitAnalyzer);
          }
        }
        fCurrentEntity.close();
      } catch (IOException ex) {
        throw new XNIException(ex);
      }
    }

    if (fEntityHandler != null) {
      //so this is the last opened entity, signal it to current fEntityHandler using Augmentation
      if (entity == null) {
        fEntityAugs.removeAllItems();
        fEntityAugs.putItem(Constants.LAST_ENTITY, Boolean.TRUE);
        fEntityHandler.endEntity(fCurrentEntity.name, fEntityAugs);
        fEntityAugs.removeAllItems();
      } else {
        fEntityHandler.endEntity(fCurrentEntity.name, null);
      }
    }
    //check if it is a document entity
    boolean documentEntity = fCurrentEntity.name == XMLEntity;

    //set popped entity as current entity
    fCurrentEntity = entity;
    fEntityScanner.setCurrentEntity(fCurrentEntity);

    //check if there are any entity left in the stack -- if there are
    //no entries EOF has been reached.
    // throw exception when it is the last entity but it is not a document entity

    if (fCurrentEntity == null & !documentEntity) {
      throw new EOFException();
    }

    if (DEBUG_BUFFER) {
      System.out.print(")endEntity: ");
      print();
      System.out.println();
    }

  } // endEntity()


  //
  // XMLComponent methods
  //
  public void reset(PropertyManager propertyManager) {
    //reset fEntityStorage
    fEntityStorage.reset(propertyManager);
    //reset XMLEntityReaderImpl
    fEntityScanner.reset(propertyManager);
    // xerces properties
    fSymbolTable = (SymbolTable) propertyManager
        .getProperty(Constants.XERCES_PROPERTY_PREFIX + Constants.SYMBOL_TABLE_PROPERTY);
    fErrorReporter = (XMLErrorReporter) propertyManager
        .getProperty(Constants.XERCES_PROPERTY_PREFIX + Constants.ERROR_REPORTER_PROPERTY);
    try {
      fStaxEntityResolver = (StaxEntityResolverWrapper) propertyManager
          .getProperty(STAX_ENTITY_RESOLVER);
    } catch (XMLConfigurationException e) {
      fStaxEntityResolver = null;
    }

    fSupportDTD = ((Boolean) propertyManager.getProperty(XMLInputFactory.SUPPORT_DTD))
        .booleanValue();
    fReplaceEntityReferences = ((Boolean) propertyManager
        .getProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES)).booleanValue();
    fSupportExternalEntities = ((Boolean) propertyManager
        .getProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES)).booleanValue();

    // Zephyr feature ignore-external-dtd is the opposite of Xerces' load-external-dtd
    fLoadExternalDTD = !((Boolean) propertyManager
        .getProperty(Constants.ZEPHYR_PROPERTY_PREFIX + Constants.IGNORE_EXTERNAL_DTD))
        .booleanValue();

    // JAXP 1.5 feature
    XMLSecurityPropertyManager spm = (XMLSecurityPropertyManager) propertyManager
        .getProperty(XML_SECURITY_PROPERTY_MANAGER);
    fAccessExternalDTD = spm.getValue(XMLSecurityPropertyManager.Property.ACCESS_EXTERNAL_DTD);

    fSecurityManager = (XMLSecurityManager) propertyManager.getProperty(SECURITY_MANAGER);

    // initialize state
    //fStandalone = false;
    fEntities.clear();
    fEntityStack.removeAllElements();
    fCurrentEntity = null;
    fValidation = false;
    fExternalGeneralEntities = true;
    fExternalParameterEntities = true;
    fAllowJavaEncodings = true;
  }

  /**
   * Resets the component. The component can query the component manager
   * about any features and properties that affect the operation of the
   * component.
   *
   * @param componentManager The component manager.
   * @throws SAXException Thrown by component on initialization error. For example, if a feature or
   * property is required for the operation of the component, the component manager may throw a
   * SAXNotRecognizedException or a SAXNotSupportedException.
   */
  public void reset(XMLComponentManager componentManager)
      throws XMLConfigurationException {

    boolean parser_settings = componentManager.getFeature(PARSER_SETTINGS, true);

    if (!parser_settings) {
      // parser settings have not been changed
      reset();
      if (fEntityScanner != null) {
        fEntityScanner.reset(componentManager);
      }
      if (fEntityStorage != null) {
        fEntityStorage.reset(componentManager);
      }
      return;
    }

    // sax features
    fValidation = componentManager.getFeature(VALIDATION, false);
    fExternalGeneralEntities = componentManager.getFeature(EXTERNAL_GENERAL_ENTITIES, true);
    fExternalParameterEntities = componentManager.getFeature(EXTERNAL_PARAMETER_ENTITIES, true);

    // xerces features
    fAllowJavaEncodings = componentManager.getFeature(ALLOW_JAVA_ENCODINGS, false);
    fWarnDuplicateEntityDef = componentManager.getFeature(WARN_ON_DUPLICATE_ENTITYDEF, false);
    fStrictURI = componentManager.getFeature(STANDARD_URI_CONFORMANT, false);
    fLoadExternalDTD = componentManager.getFeature(LOAD_EXTERNAL_DTD, true);

    // xerces properties
    fSymbolTable = (SymbolTable) componentManager.getProperty(SYMBOL_TABLE);
    fErrorReporter = (XMLErrorReporter) componentManager.getProperty(ERROR_REPORTER);
    fEntityResolver = (XMLEntityResolver) componentManager.getProperty(ENTITY_RESOLVER, null);
    fStaxEntityResolver = (StaxEntityResolverWrapper) componentManager
        .getProperty(STAX_ENTITY_RESOLVER, null);
    fValidationManager = (ValidationManager) componentManager.getProperty(VALIDATION_MANAGER, null);
    fSecurityManager = (XMLSecurityManager) componentManager.getProperty(SECURITY_MANAGER, null);
    entityExpansionIndex = fSecurityManager.getIndex(Constants.JDK_ENTITY_EXPANSION_LIMIT);

    //StAX Property
    fSupportDTD = true;
    fReplaceEntityReferences = true;
    fSupportExternalEntities = true;

    // JAXP 1.5 feature
    XMLSecurityPropertyManager spm = (XMLSecurityPropertyManager) componentManager
        .getProperty(XML_SECURITY_PROPERTY_MANAGER, null);
    if (spm == null) {
      spm = new XMLSecurityPropertyManager();
    }
    fAccessExternalDTD = spm.getValue(XMLSecurityPropertyManager.Property.ACCESS_EXTERNAL_DTD);

    //reset general state
    reset();

    fEntityScanner.reset(componentManager);
    fEntityStorage.reset(componentManager);

  } // reset(XMLComponentManager)

  // reset general state.  Should not be called other than by
  // a class acting as a component manager but not
  // implementing that interface for whatever reason.
  public void reset() {

    // initialize state
    fStandalone = false;
    fEntities.clear();
    fEntityStack.removeAllElements();
    fEntityExpansionCount = 0;

    fCurrentEntity = null;
    // reset scanner
    if (fXML10EntityScanner != null) {
      fXML10EntityScanner.reset(fSymbolTable, this, fErrorReporter);
    }
    if (fXML11EntityScanner != null) {
      fXML11EntityScanner.reset(fSymbolTable, this, fErrorReporter);
    }

    // DEBUG
    if (DEBUG_ENTITIES) {
      addInternalEntity("text", "Hello, World.");
      addInternalEntity("empty-element", "<foo/>");
      addInternalEntity("balanced-element", "<foo></foo>");
      addInternalEntity("balanced-element-with-text", "<foo>Hello, World</foo>");
      addInternalEntity("balanced-element-with-entity", "<foo>&text;</foo>");
      addInternalEntity("unbalanced-entity", "<foo>");
      addInternalEntity("recursive-entity", "<foo>&recursive-entity2;</foo>");
      addInternalEntity("recursive-entity2", "<bar>&recursive-entity3;</bar>");
      addInternalEntity("recursive-entity3", "<baz>&recursive-entity;</baz>");
      try {
        addExternalEntity("external-text", null, "external-text.ent", "test/external-text.xml");
        addExternalEntity("external-balanced-element", null, "external-balanced-element.ent",
            "test/external-balanced-element.xml");
        addExternalEntity("one", null, "ent/one.ent", "test/external-entity.xml");
        addExternalEntity("two", null, "ent/two.ent", "test/ent/one.xml");
      } catch (IOException ex) {
        // should never happen
      }
    }

    fEntityHandler = null;

    // reset scanner
    //if(fEntityScanner!=null)
    //  fEntityScanner.reset(fSymbolTable, this,fErrorReporter);

  }

  /**
   * Returns a list of feature identifiers that are recognized by
   * this component. This method may return null if no features
   * are recognized by this component.
   */
  public String[] getRecognizedFeatures() {
    return (String[]) (RECOGNIZED_FEATURES.clone());
  } // getRecognizedFeatures():String[]

  /**
   * Sets the state of a feature. This method is called by the component
   * manager any time after reset when a feature changes state.
   * <p>
   * <strong>Note:</strong> Components should silently ignore features
   * that do not affect the operation of the component.
   *
   * @param featureId The feature identifier.
   * @param state The state of the feature.
   * @throws SAXNotRecognizedException The component should not throw this exception.
   * @throws SAXNotSupportedException The component should not throw this exception.
   */
  public void setFeature(String featureId, boolean state)
      throws XMLConfigurationException {

    // xerces features
    if (featureId.startsWith(Constants.XERCES_FEATURE_PREFIX)) {
      final int suffixLength = featureId.length() - Constants.XERCES_FEATURE_PREFIX.length();
      if (suffixLength == Constants.ALLOW_JAVA_ENCODINGS_FEATURE.length() &&
          featureId.endsWith(Constants.ALLOW_JAVA_ENCODINGS_FEATURE)) {
        fAllowJavaEncodings = state;
      }
      if (suffixLength == Constants.LOAD_EXTERNAL_DTD_FEATURE.length() &&
          featureId.endsWith(Constants.LOAD_EXTERNAL_DTD_FEATURE)) {
        fLoadExternalDTD = state;
        return;
      }
    }

  } // setFeature(String,boolean)

  /**
   * Sets the value of a property. This method is called by the component
   * manager any time after reset when a property changes value.
   * <p>
   * <strong>Note:</strong> Components should silently ignore properties
   * that do not affect the operation of the component.
   *
   * @param propertyId The property identifier.
   * @param value The value of the property.
   * @throws SAXNotRecognizedException The component should not throw this exception.
   * @throws SAXNotSupportedException The component should not throw this exception.
   */
  public void setProperty(String propertyId, Object value) {
    // Xerces properties
    if (propertyId.startsWith(Constants.XERCES_PROPERTY_PREFIX)) {
      final int suffixLength = propertyId.length() - Constants.XERCES_PROPERTY_PREFIX.length();

      if (suffixLength == Constants.SYMBOL_TABLE_PROPERTY.length() &&
          propertyId.endsWith(Constants.SYMBOL_TABLE_PROPERTY)) {
        fSymbolTable = (SymbolTable) value;
        return;
      }
      if (suffixLength == Constants.ERROR_REPORTER_PROPERTY.length() &&
          propertyId.endsWith(Constants.ERROR_REPORTER_PROPERTY)) {
        fErrorReporter = (XMLErrorReporter) value;
        return;
      }
      if (suffixLength == Constants.ENTITY_RESOLVER_PROPERTY.length() &&
          propertyId.endsWith(Constants.ENTITY_RESOLVER_PROPERTY)) {
        fEntityResolver = (XMLEntityResolver) value;
        return;
      }
      if (suffixLength == Constants.BUFFER_SIZE_PROPERTY.length() &&
          propertyId.endsWith(Constants.BUFFER_SIZE_PROPERTY)) {
        Integer bufferSize = (Integer) value;
        if (bufferSize != null &&
            bufferSize.intValue() > DEFAULT_XMLDECL_BUFFER_SIZE) {
          fBufferSize = bufferSize.intValue();
          fEntityScanner.setBufferSize(fBufferSize);
          fBufferPool.setExternalBufferSize(fBufferSize);
        }
      }
      if (suffixLength == Constants.SECURITY_MANAGER_PROPERTY.length() &&
          propertyId.endsWith(Constants.SECURITY_MANAGER_PROPERTY)) {
        fSecurityManager = (XMLSecurityManager) value;
      }
    }

    //JAXP 1.5 properties
    if (propertyId.equals(XML_SECURITY_PROPERTY_MANAGER)) {
      XMLSecurityPropertyManager spm = (XMLSecurityPropertyManager) value;
      fAccessExternalDTD = spm.getValue(XMLSecurityPropertyManager.Property.ACCESS_EXTERNAL_DTD);
    }
  }

  public void setLimitAnalyzer(XMLLimitAnalyzer fLimitAnalyzer) {
    this.fLimitAnalyzer = fLimitAnalyzer;
  }

  /**
   * Returns a list of property identifiers that are recognized by
   * this component. This method may return null if no properties
   * are recognized by this component.
   */
  public String[] getRecognizedProperties() {
    return (String[]) (RECOGNIZED_PROPERTIES.clone());
  } // getRecognizedProperties():String[]

  /**
   * Returns the default state for a feature, or null if this
   * component does not want to report a default value for this
   * feature.
   *
   * @param featureId The feature identifier.
   * @since Xerces 2.2.0
   */
  public Boolean getFeatureDefault(String featureId) {
    for (int i = 0; i < RECOGNIZED_FEATURES.length; i++) {
      if (RECOGNIZED_FEATURES[i].equals(featureId)) {
        return FEATURE_DEFAULTS[i];
      }
    }
    return null;
  } // getFeatureDefault(String):Boolean

  /**
   * Returns the default state for a property, or null if this
   * component does not want to report a default value for this
   * property.
   *
   * @param propertyId The property identifier.
   * @since Xerces 2.2.0
   */
  public Object getPropertyDefault(String propertyId) {
    for (int i = 0; i < RECOGNIZED_PROPERTIES.length; i++) {
      if (RECOGNIZED_PROPERTIES[i].equals(propertyId)) {
        return PROPERTY_DEFAULTS[i];
      }
    }
    return null;
  } // getPropertyDefault(String):Object

  //
  // Public static methods
  //

  /**
   * Expands a system id and returns the system id as a URI, if
   * it can be expanded. A return value of null means that the
   * identifier is already expanded. An exception thrown
   * indicates a failure to expand the id.
   *
   * @param systemId The systemId to be expanded.
   * @return Returns the URI string representing the expanded system identifier. A null value
   * indicates that the given system identifier is already expanded.
   */
  public static String expandSystemId(String systemId) {
    return expandSystemId(systemId, null);
  } // expandSystemId(String):String

  //
  // Public static methods
  //

  // current value of the "user.dir" property
  private static String gUserDir;
  // cached URI object for the current value of the escaped "user.dir" property stored as a URI
  private static URI gUserDirURI;
  // which ASCII characters need to be escaped
  private static boolean gNeedEscaping[] = new boolean[128];
  // the first hex character if a character needs to be escaped
  private static char gAfterEscaping1[] = new char[128];
  // the second hex character if a character needs to be escaped
  private static char gAfterEscaping2[] = new char[128];
  private static char[] gHexChs = {'0', '1', '2', '3', '4', '5', '6', '7',
      '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

  // initialize the above 3 arrays
  static {
    for (int i = 0; i <= 0x1f; i++) {
      gNeedEscaping[i] = true;
      gAfterEscaping1[i] = gHexChs[i >> 4];
      gAfterEscaping2[i] = gHexChs[i & 0xf];
    }
    gNeedEscaping[0x7f] = true;
    gAfterEscaping1[0x7f] = '7';
    gAfterEscaping2[0x7f] = 'F';
    char[] escChs = {' ', '<', '>', '#', '%', '"', '{', '}',
        '|', '\\', '^', '~', '[', ']', '`'};
    int len = escChs.length;
    char ch;
    for (int i = 0; i < len; i++) {
      ch = escChs[i];
      gNeedEscaping[ch] = true;
      gAfterEscaping1[ch] = gHexChs[ch >> 4];
      gAfterEscaping2[ch] = gHexChs[ch & 0xf];
    }
  }

  // To escape the "user.dir" system property, by using %HH to represent
  // special ASCII characters: 0x00~0x1F, 0x7F, ' ', '<', '>', '#', '%'
  // and '"'. It's a static method, so needs to be synchronized.
  // this method looks heavy, but since the system property isn't expected
  // to change often, so in most cases, we only need to return the URI
  // that was escaped before.
  // According to the URI spec, non-ASCII characters (whose value >= 128)
  // need to be escaped too.
  // REVISIT: don't know how to escape non-ASCII characters, especially
  // which encoding to use. Leave them for now.
  private static synchronized URI getUserDir() throws URI.MalformedURIException {
    // get the user.dir property
    String userDir = "";
    try {
      userDir = SecuritySupport.getSystemProperty("user.dir");
    } catch (SecurityException se) {
    }

    // return empty string if property value is empty string.
    if (userDir.length() == 0) {
      return new URI("file", "", "", null, null);
    }
    // compute the new escaped value if the new property value doesn't
    // match the previous one
    if (gUserDirURI != null && userDir.equals(gUserDir)) {
      return gUserDirURI;
    }

    // record the new value as the global property value
    gUserDir = userDir;

    char separator = java.io.File.separatorChar;
    userDir = userDir.replace(separator, '/');

    int len = userDir.length(), ch;
    StringBuffer buffer = new StringBuffer(len * 3);
    // change C:/blah to /C:/blah
    if (len >= 2 && userDir.charAt(1) == ':') {
      ch = Character.toUpperCase(userDir.charAt(0));
      if (ch >= 'A' && ch <= 'Z') {
        buffer.append('/');
      }
    }

    // for each character in the path
    int i = 0;
    for (; i < len; i++) {
      ch = userDir.charAt(i);
      // if it's not an ASCII character, break here, and use UTF-8 encoding
      if (ch >= 128) {
        break;
      }
      if (gNeedEscaping[ch]) {
        buffer.append('%');
        buffer.append(gAfterEscaping1[ch]);
        buffer.append(gAfterEscaping2[ch]);
        // record the fact that it's escaped
      } else {
        buffer.append((char) ch);
      }
    }

    // we saw some non-ascii character
    if (i < len) {
      // get UTF-8 bytes for the remaining sub-string
      byte[] bytes = null;
      byte b;
      try {
        bytes = userDir.substring(i).getBytes("UTF-8");
      } catch (java.io.UnsupportedEncodingException e) {
        // should never happen
        return new URI("file", "", userDir, null, null);
      }
      len = bytes.length;

      // for each byte
      for (i = 0; i < len; i++) {
        b = bytes[i];
        // for non-ascii character: make it positive, then escape
        if (b < 0) {
          ch = b + 256;
          buffer.append('%');
          buffer.append(gHexChs[ch >> 4]);
          buffer.append(gHexChs[ch & 0xf]);
        } else if (gNeedEscaping[b]) {
          buffer.append('%');
          buffer.append(gAfterEscaping1[b]);
          buffer.append(gAfterEscaping2[b]);
        } else {
          buffer.append((char) b);
        }
      }
    }

    // change blah/blah to blah/blah/
    if (!userDir.endsWith("/")) {
      buffer.append('/');
    }

    gUserDirURI = new URI("file", "", buffer.toString(), null, null);

    return gUserDirURI;
  }

  /**
   * Absolutizes a URI using the current value
   * of the "user.dir" property as the base URI. If
   * the URI is already absolute, this is a no-op.
   *
   * @param uri the URI to absolutize
   */
  public static void absolutizeAgainstUserDir(URI uri)
      throws URI.MalformedURIException {
    uri.absolutize(getUserDir());
  }

  /**
   * Expands a system id and returns the system id as a URI, if
   * it can be expanded. A return value of null means that the
   * identifier is already expanded. An exception thrown
   * indicates a failure to expand the id.
   *
   * @param systemId The systemId to be expanded.
   * @return Returns the URI string representing the expanded system identifier. A null value
   * indicates that the given system identifier is already expanded.
   */
  public static String expandSystemId(String systemId, String baseSystemId) {

    // check for bad parameters id
    if (systemId == null || systemId.length() == 0) {
      return systemId;
    }
    // if id already expanded, return
    try {
      URI uri = new URI(systemId);
      if (uri != null) {
        return systemId;
      }
    } catch (URI.MalformedURIException e) {
      // continue on...
    }
    // normalize id
    String id = fixURI(systemId);

    // normalize base
    URI base = null;
    URI uri = null;
    try {
      if (baseSystemId == null || baseSystemId.length() == 0 ||
          baseSystemId.equals(systemId)) {
        String dir = getUserDir().toString();
        base = new URI("file", "", dir, null, null);
      } else {
        try {
          base = new URI(fixURI(baseSystemId));
        } catch (URI.MalformedURIException e) {
          if (baseSystemId.indexOf(':') != -1) {
            // for xml schemas we might have baseURI with
            // a specified drive
            base = new URI("file", "", fixURI(baseSystemId), null, null);
          } else {
            String dir = getUserDir().toString();
            dir = dir + fixURI(baseSystemId);
            base = new URI("file", "", dir, null, null);
          }
        }
      }
      // expand id
      uri = new URI(base, id);
    } catch (Exception e) {
      // let it go through

    }

    if (uri == null) {
      return systemId;
    }
    return uri.toString();

  } // expandSystemId(String,String):String

  /**
   * Expands a system id and returns the system id as a URI, if
   * it can be expanded. A return value of null means that the
   * identifier is already expanded. An exception thrown
   * indicates a failure to expand the id.
   *
   * @param systemId The systemId to be expanded.
   * @return Returns the URI string representing the expanded system identifier. A null value
   * indicates that the given system identifier is already expanded.
   */
  public static String expandSystemId(String systemId, String baseSystemId,
      boolean strict)
      throws URI.MalformedURIException {

    // check if there is a system id before
    // trying to expand it.
    if (systemId == null) {
      return null;
    }

    // system id has to be a valid URI
    if (strict) {

      // check if there is a system id before
      // trying to expand it.
      if (systemId == null) {
        return null;
      }

      try {
        // if it's already an absolute one, return it
        new URI(systemId);
        return systemId;
      } catch (URI.MalformedURIException ex) {
      }
      URI base = null;
      // if there isn't a base uri, use the working directory
      if (baseSystemId == null || baseSystemId.length() == 0) {
        base = new URI("file", "", getUserDir().toString(), null, null);
      }
      // otherwise, use the base uri
      else {
        try {
          base = new URI(baseSystemId);
        } catch (URI.MalformedURIException e) {
          // assume "base" is also a relative uri
          String dir = getUserDir().toString();
          dir = dir + baseSystemId;
          base = new URI("file", "", dir, null, null);
        }
      }
      // absolutize the system id using the base
      URI uri = new URI(base, systemId);
      // return the string rep of the new uri (an absolute one)
      return uri.toString();

      // if any exception is thrown, it'll get thrown to the caller.
    }

    // Assume the URIs are well-formed. If it turns out they're not, try fixing them up.
    try {
      return expandSystemIdStrictOff(systemId, baseSystemId);
    } catch (URI.MalformedURIException e) {
      /** Xerces URI rejects unicode, try java.net.URI
       * this is not ideal solution, but it covers known cases which either
       * Xerces URI or java.net.URI can handle alone
       * will file bug against java.net.URI
       */
      try {
        return expandSystemIdStrictOff1(systemId, baseSystemId);
      } catch (URISyntaxException ex) {
        // continue on...
      }
    }
    // check for bad parameters id
    if (systemId.length() == 0) {
      return systemId;
    }

    // normalize id
    String id = fixURI(systemId);

    // normalize base
    URI base = null;
    URI uri = null;
    try {
      if (baseSystemId == null || baseSystemId.length() == 0 ||
          baseSystemId.equals(systemId)) {
        base = getUserDir();
      } else {
        try {
          base = new URI(fixURI(baseSystemId).trim());
        } catch (URI.MalformedURIException e) {
          if (baseSystemId.indexOf(':') != -1) {
            // for xml schemas we might have baseURI with
            // a specified drive
            base = new URI("file", "", fixURI(baseSystemId).trim(), null, null);
          } else {
            base = new URI(getUserDir(), fixURI(baseSystemId));
          }
        }
      }
      // expand id
      uri = new URI(base, id.trim());
    } catch (Exception e) {
      // let it go through

    }

    if (uri == null) {
      return systemId;
    }
    return uri.toString();

  } // expandSystemId(String,String,boolean):String

  /**
   * Helper method for expandSystemId(String,String,boolean):String
   */
  private static String expandSystemIdStrictOn(String systemId, String baseSystemId)
      throws URI.MalformedURIException {

    URI systemURI = new URI(systemId, true);
    // If it's already an absolute one, return it
    if (systemURI.isAbsoluteURI()) {
      return systemId;
    }

    // If there isn't a base URI, use the working directory
    URI baseURI = null;
    if (baseSystemId == null || baseSystemId.length() == 0) {
      baseURI = getUserDir();
    } else {
      baseURI = new URI(baseSystemId, true);
      if (!baseURI.isAbsoluteURI()) {
        // assume "base" is also a relative uri
        baseURI.absolutize(getUserDir());
      }
    }

    // absolutize the system identifier using the base URI
    systemURI.absolutize(baseURI);

    // return the string rep of the new uri (an absolute one)
    return systemURI.toString();

    // if any exception is thrown, it'll get thrown to the caller.

  } // expandSystemIdStrictOn(String,String):String

  /**
   * Attempt to set whether redirects will be followed for an <code>HttpURLConnection</code>.
   * This may fail on earlier JDKs which do not support setting this preference.
   */
  public static void setInstanceFollowRedirects(HttpURLConnection urlCon, boolean followRedirects) {
    try {
      Method method = HttpURLConnection.class
          .getMethod("setInstanceFollowRedirects", new Class[]{Boolean.TYPE});
      method.invoke(urlCon, new Object[]{followRedirects ? Boolean.TRUE : Boolean.FALSE});
    }
    // setInstanceFollowRedirects doesn't exist.
    catch (Exception exc) {
    }
  }


  /**
   * Helper method for expandSystemId(String,String,boolean):String
   */
  private static String expandSystemIdStrictOff(String systemId, String baseSystemId)
      throws URI.MalformedURIException {

    URI systemURI = new URI(systemId, true);
    // If it's already an absolute one, return it
    if (systemURI.isAbsoluteURI()) {
      if (systemURI.getScheme().length() > 1) {
        return systemId;
      }
      /**
       * If the scheme's length is only one character,
       * it's likely that this was intended as a file
       * path. Fixing this up in expandSystemId to
       * maintain backwards compatibility.
       */
      throw new URI.MalformedURIException();
    }

    // If there isn't a base URI, use the working directory
    URI baseURI = null;
    if (baseSystemId == null || baseSystemId.length() == 0) {
      baseURI = getUserDir();
    } else {
      baseURI = new URI(baseSystemId, true);
      if (!baseURI.isAbsoluteURI()) {
        // assume "base" is also a relative uri
        baseURI.absolutize(getUserDir());
      }
    }

    // absolutize the system identifier using the base URI
    systemURI.absolutize(baseURI);

    // return the string rep of the new uri (an absolute one)
    return systemURI.toString();

    // if any exception is thrown, it'll get thrown to the caller.

  } // expandSystemIdStrictOff(String,String):String

  private static String expandSystemIdStrictOff1(String systemId, String baseSystemId)
      throws URISyntaxException, URI.MalformedURIException {

    java.net.URI systemURI = new java.net.URI(systemId);
    // If it's already an absolute one, return it
    if (systemURI.isAbsolute()) {
      if (systemURI.getScheme().length() > 1) {
        return systemId;
      }
      /**
       * If the scheme's length is only one character,
       * it's likely that this was intended as a file
       * path. Fixing this up in expandSystemId to
       * maintain backwards compatibility.
       */
      throw new URISyntaxException(systemId, "the scheme's length is only one character");
    }

    // If there isn't a base URI, use the working directory
    URI baseURI = null;
    if (baseSystemId == null || baseSystemId.length() == 0) {
      baseURI = getUserDir();
    } else {
      baseURI = new URI(baseSystemId, true);
      if (!baseURI.isAbsoluteURI()) {
        // assume "base" is also a relative uri
        baseURI.absolutize(getUserDir());
      }
    }

    // absolutize the system identifier using the base URI
//        systemURI.absolutize(baseURI);
    systemURI = (new java.net.URI(baseURI.toString())).resolve(systemURI);

    // return the string rep of the new uri (an absolute one)
    return systemURI.toString();

    // if any exception is thrown, it'll get thrown to the caller.

  } // expandSystemIdStrictOff(String,String):String

  //
  // Protected methods
  //


  /**
   * Returns the IANA encoding name that is auto-detected from
   * the bytes specified, with the endian-ness of that encoding where appropriate.
   *
   * @param b4 The first four bytes of the input.
   * @param count The number of bytes actually read.
   * @return a 2-element array:  the first element, an IANA-encoding string, the second element a
   * Boolean which is true iff the document is big endian, false if it's little-endian, and null if
   * the distinction isn't relevant.
   */
  protected Object[] getEncodingName(byte[] b4, int count) {

    if (count < 2) {
      return defaultEncoding;
    }

    // UTF-16, with BOM
    int b0 = b4[0] & 0xFF;
    int b1 = b4[1] & 0xFF;
    if (b0 == 0xFE && b1 == 0xFF) {
      // UTF-16, big-endian
      return new Object[]{"UTF-16BE", new Boolean(true)};
    }
    if (b0 == 0xFF && b1 == 0xFE) {
      // UTF-16, little-endian
      return new Object[]{"UTF-16LE", new Boolean(false)};
    }

    // default to UTF-8 if we don't have enough bytes to make a
    // good determination of the encoding
    if (count < 3) {
      return defaultEncoding;
    }

    // UTF-8 with a BOM
    int b2 = b4[2] & 0xFF;
    if (b0 == 0xEF && b1 == 0xBB && b2 == 0xBF) {
      return defaultEncoding;
    }

    // default to UTF-8 if we don't have enough bytes to make a
    // good determination of the encoding
    if (count < 4) {
      return defaultEncoding;
    }

    // other encodings
    int b3 = b4[3] & 0xFF;
    if (b0 == 0x00 && b1 == 0x00 && b2 == 0x00 && b3 == 0x3C) {
      // UCS-4, big endian (1234)
      return new Object[]{"ISO-10646-UCS-4", new Boolean(true)};
    }
    if (b0 == 0x3C && b1 == 0x00 && b2 == 0x00 && b3 == 0x00) {
      // UCS-4, little endian (4321)
      return new Object[]{"ISO-10646-UCS-4", new Boolean(false)};
    }
    if (b0 == 0x00 && b1 == 0x00 && b2 == 0x3C && b3 == 0x00) {
      // UCS-4, unusual octet order (2143)
      // REVISIT: What should this be?
      return new Object[]{"ISO-10646-UCS-4", null};
    }
    if (b0 == 0x00 && b1 == 0x3C && b2 == 0x00 && b3 == 0x00) {
      // UCS-4, unusual octect order (3412)
      // REVISIT: What should this be?
      return new Object[]{"ISO-10646-UCS-4", null};
    }
    if (b0 == 0x00 && b1 == 0x3C && b2 == 0x00 && b3 == 0x3F) {
      // UTF-16, big-endian, no BOM
      // (or could turn out to be UCS-2...
      // REVISIT: What should this be?
      return new Object[]{"UTF-16BE", new Boolean(true)};
    }
    if (b0 == 0x3C && b1 == 0x00 && b2 == 0x3F && b3 == 0x00) {
      // UTF-16, little-endian, no BOM
      // (or could turn out to be UCS-2...
      return new Object[]{"UTF-16LE", new Boolean(false)};
    }
    if (b0 == 0x4C && b1 == 0x6F && b2 == 0xA7 && b3 == 0x94) {
      // EBCDIC
      // a la xerces1, return CP037 instead of EBCDIC here
      return new Object[]{"CP037", null};
    }

    return defaultEncoding;

  } // getEncodingName(byte[],int):Object[]

  /**
   * Creates a reader capable of reading the given input stream in
   * the specified encoding.
   *
   * @param inputStream The input stream.
   * @param encoding The encoding name that the input stream is encoded using. If the user has
   * specified that Java encoding names are allowed, then the encoding name may be a Java encoding
   * name; otherwise, it is an ianaEncoding name.
   * @param isBigEndian For encodings (like uCS-4), whose names cannot specify a byte order, this
   * tells whether the order is bigEndian.  null menas unknown or not relevant.
   * @return Returns a reader.
   */
  protected Reader createReader(InputStream inputStream, String encoding, Boolean isBigEndian)
      throws IOException {

    // normalize encoding name
    if (encoding == null) {
      encoding = "UTF-8";
    }

    // try to use an optimized reader
    String ENCODING = encoding.toUpperCase(Locale.ENGLISH);
    if (ENCODING.equals("UTF-8")) {
      if (DEBUG_ENCODINGS) {
        System.out.println("$$$ creating UTF8Reader");
      }
      return new UTF8Reader(inputStream, fBufferSize,
          fErrorReporter.getMessageFormatter(XMLMessageFormatter.XML_DOMAIN),
          fErrorReporter.getLocale());
    }
    if (ENCODING.equals("US-ASCII")) {
      if (DEBUG_ENCODINGS) {
        System.out.println("$$$ creating ASCIIReader");
      }
      return new ASCIIReader(inputStream, fBufferSize,
          fErrorReporter.getMessageFormatter(XMLMessageFormatter.XML_DOMAIN),
          fErrorReporter.getLocale());
    }
    if (ENCODING.equals("ISO-10646-UCS-4")) {
      if (isBigEndian != null) {
        boolean isBE = isBigEndian.booleanValue();
        if (isBE) {
          return new UCSReader(inputStream, UCSReader.UCS4BE);
        } else {
          return new UCSReader(inputStream, UCSReader.UCS4LE);
        }
      } else {
        fErrorReporter.reportError(this.getEntityScanner(), XMLMessageFormatter.XML_DOMAIN,
            "EncodingByteOrderUnsupported",
            new Object[]{encoding},
            XMLErrorReporter.SEVERITY_FATAL_ERROR);
      }
    }
    if (ENCODING.equals("ISO-10646-UCS-2")) {
      if (isBigEndian != null) { // sould never happen with this encoding...
        boolean isBE = isBigEndian.booleanValue();
        if (isBE) {
          return new UCSReader(inputStream, UCSReader.UCS2BE);
        } else {
          return new UCSReader(inputStream, UCSReader.UCS2LE);
        }
      } else {
        fErrorReporter.reportError(this.getEntityScanner(), XMLMessageFormatter.XML_DOMAIN,
            "EncodingByteOrderUnsupported",
            new Object[]{encoding},
            XMLErrorReporter.SEVERITY_FATAL_ERROR);
      }
    }

    // check for valid name
    boolean validIANA = XMLChar.isValidIANAEncoding(encoding);
    boolean validJava = XMLChar.isValidJavaEncoding(encoding);
    if (!validIANA || (fAllowJavaEncodings && !validJava)) {
      fErrorReporter.reportError(this.getEntityScanner(), XMLMessageFormatter.XML_DOMAIN,
          "EncodingDeclInvalid",
          new Object[]{encoding},
          XMLErrorReporter.SEVERITY_FATAL_ERROR);
      // NOTE: AndyH suggested that, on failure, we use ISO Latin 1
      //       because every byte is a valid ISO Latin 1 character.
      //       It may not translate correctly but if we failed on
      //       the encoding anyway, then we're expecting the content
      //       of the document to be bad. This will just prevent an
      //       invalid UTF-8 sequence to be detected. This is only
      //       important when continue-after-fatal-error is turned
      //       on. -Ac
      encoding = "ISO-8859-1";
    }

    // try to use a Java reader
    String javaEncoding = EncodingMap.getIANA2JavaMapping(ENCODING);
    if (javaEncoding == null) {
      if (fAllowJavaEncodings) {
        javaEncoding = encoding;
      } else {
        fErrorReporter.reportError(this.getEntityScanner(), XMLMessageFormatter.XML_DOMAIN,
            "EncodingDeclInvalid",
            new Object[]{encoding},
            XMLErrorReporter.SEVERITY_FATAL_ERROR);
        // see comment above.
        javaEncoding = "ISO8859_1";
      }
    }
    if (DEBUG_ENCODINGS) {
      System.out.print("$$$ creating Java InputStreamReader: encoding=" + javaEncoding);
      if (javaEncoding == encoding) {
        System.out.print(" (IANA encoding)");
      }
      System.out.println();
    }
    return new BufferedReader(new InputStreamReader(inputStream, javaEncoding));

  } // createReader(InputStream,String, Boolean): Reader


  /**
   * Return the public identifier for the current document event.
   * <p>
   * The return value is the public identifier of the document
   * entity or of the external parsed entity in which the markup
   * triggering the event appears.
   *
   * @return A string containing the public identifier, or null if none is available.
   */
  public String getPublicId() {
    return (fCurrentEntity != null && fCurrentEntity.entityLocation != null)
        ? fCurrentEntity.entityLocation.getPublicId() : null;
  } // getPublicId():String

  /**
   * Return the expanded system identifier for the current document event.
   * <p>
   * The return value is the expanded system identifier of the document
   * entity or of the external parsed entity in which the markup
   * triggering the event appears.
   * <p>
   * If the system identifier is a URL, the parser must resolve it
   * fully before passing it to the application.
   *
   * @return A string containing the expanded system identifier, or null if none is available.
   */
  public String getExpandedSystemId() {
    if (fCurrentEntity != null) {
      if (fCurrentEntity.entityLocation != null &&
          fCurrentEntity.entityLocation.getExpandedSystemId() != null) {
        return fCurrentEntity.entityLocation.getExpandedSystemId();
      } else {
        // search for the first external entity on the stack
        int size = fEntityStack.size();
        for (int i = size - 1; i >= 0; i--) {
          Entity.ScannedEntity externalEntity =
              (Entity.ScannedEntity) fEntityStack.elementAt(i);

          if (externalEntity.entityLocation != null &&
              externalEntity.entityLocation.getExpandedSystemId() != null) {
            return externalEntity.entityLocation.getExpandedSystemId();
          }
        }
      }
    }
    return null;
  } // getExpandedSystemId():String

  /**
   * Return the literal system identifier for the current document event.
   * <p>
   * The return value is the literal system identifier of the document
   * entity or of the external parsed entity in which the markup
   * triggering the event appears.
   * <p>
   *
   * @return A string containing the literal system identifier, or null if none is available.
   */
  public String getLiteralSystemId() {
    if (fCurrentEntity != null) {
      if (fCurrentEntity.entityLocation != null &&
          fCurrentEntity.entityLocation.getLiteralSystemId() != null) {
        return fCurrentEntity.entityLocation.getLiteralSystemId();
      } else {
        // search for the first external entity on the stack
        int size = fEntityStack.size();
        for (int i = size - 1; i >= 0; i--) {
          Entity.ScannedEntity externalEntity =
              (Entity.ScannedEntity) fEntityStack.elementAt(i);

          if (externalEntity.entityLocation != null &&
              externalEntity.entityLocation.getLiteralSystemId() != null) {
            return externalEntity.entityLocation.getLiteralSystemId();
          }
        }
      }
    }
    return null;
  } // getLiteralSystemId():String

  /**
   * Return the line number where the current document event ends.
   * <p>
   * <strong>Warning:</strong> The return value from the method
   * is intended only as an approximation for the sake of error
   * reporting; it is not intended to provide sufficient information
   * to edit the character content of the original XML document.
   * <p>
   * The return value is an approximation of the line number
   * in the document entity or external parsed entity where the
   * markup triggering the event appears.
   * <p>
   * If possible, the SAX driver should provide the line position
   * of the first character after the text associated with the document
   * event.  The first line in the document is line 1.
   *
   * @return The line number, or -1 if none is available.
   */
  public int getLineNumber() {
    if (fCurrentEntity != null) {
      if (fCurrentEntity.isExternal()) {
        return fCurrentEntity.lineNumber;
      } else {
        // search for the first external entity on the stack
        int size = fEntityStack.size();
        for (int i = size - 1; i > 0; i--) {
          Entity.ScannedEntity firstExternalEntity = (Entity.ScannedEntity) fEntityStack
              .elementAt(i);
          if (firstExternalEntity.isExternal()) {
            return firstExternalEntity.lineNumber;
          }
        }
      }
    }

    return -1;

  } // getLineNumber():int

  /**
   * Return the column number where the current document event ends.
   * <p>
   * <strong>Warning:</strong> The return value from the method
   * is intended only as an approximation for the sake of error
   * reporting; it is not intended to provide sufficient information
   * to edit the character content of the original XML document.
   * <p>
   * The return value is an approximation of the column number
   * in the document entity or external parsed entity where the
   * markup triggering the event appears.
   * <p>
   * If possible, the SAX driver should provide the line position
   * of the first character after the text associated with the document
   * event.
   * <p>
   * If possible, the SAX driver should provide the line position
   * of the first character after the text associated with the document
   * event.  The first column in each line is column 1.
   *
   * @return The column number, or -1 if none is available.
   */
  public int getColumnNumber() {
    if (fCurrentEntity != null) {
      if (fCurrentEntity.isExternal()) {
        return fCurrentEntity.columnNumber;
      } else {
        // search for the first external entity on the stack
        int size = fEntityStack.size();
        for (int i = size - 1; i > 0; i--) {
          Entity.ScannedEntity firstExternalEntity = (Entity.ScannedEntity) fEntityStack
              .elementAt(i);
          if (firstExternalEntity.isExternal()) {
            return firstExternalEntity.columnNumber;
          }
        }
      }
    }

    return -1;
  } // getColumnNumber():int

  //
  // Protected static methods
  //

  /**
   * Fixes a platform dependent filename to standard URI form.
   *
   * @param str The string to fix.
   * @return Returns the fixed URI string.
   */
  protected static String fixURI(String str) {

    // handle platform dependent strings
    str = str.replace(java.io.File.separatorChar, '/');

    // Windows fix
    if (str.length() >= 2) {
      char ch1 = str.charAt(1);
      // change "C:blah" to "/C:blah"
      if (ch1 == ':') {
        char ch0 = Character.toUpperCase(str.charAt(0));
        if (ch0 >= 'A' && ch0 <= 'Z') {
          str = "/" + str;
        }
      }
      // change "//blah" to "file://blah"
      else if (ch1 == '/' && str.charAt(0) == '/') {
        str = "file:" + str;
      }
    }

    // replace spaces in file names with %20.
    // Original comment from JDK5: the following algorithm might not be
    // very performant, but people who want to use invalid URI's have to
    // pay the price.
    int pos = str.indexOf(' ');
    if (pos >= 0) {
      StringBuilder sb = new StringBuilder(str.length());
      // put characters before ' ' into the string builder
      for (int i = 0; i < pos; i++) {
        sb.append(str.charAt(i));
      }
      // and %20 for the space
      sb.append("%20");
      // for the remamining part, also convert ' ' to "%20".
      for (int i = pos + 1; i < str.length(); i++) {
        if (str.charAt(i) == ' ') {
          sb.append("%20");
        } else {
          sb.append(str.charAt(i));
        }
      }
      str = sb.toString();
    }

    // done
    return str;

  } // fixURI(String):String

  //
  // Package visible methods
  //

  /**
   * Prints the contents of the buffer.
   */
  final void print() {
    if (DEBUG_BUFFER) {
      if (fCurrentEntity != null) {
        System.out.print('[');
        System.out.print(fCurrentEntity.count);
        System.out.print(' ');
        System.out.print(fCurrentEntity.position);
        if (fCurrentEntity.count > 0) {
          System.out.print(" \"");
          for (int i = 0; i < fCurrentEntity.count; i++) {
            if (i == fCurrentEntity.position) {
              System.out.print('^');
            }
            char c = fCurrentEntity.ch[i];
            switch (c) {
              case '\n': {
                System.out.print("\\n");
                break;
              }
              case '\r': {
                System.out.print("\\r");
                break;
              }
              case '\t': {
                System.out.print("\\t");
                break;
              }
              case '\\': {
                System.out.print("\\\\");
                break;
              }
              default: {
                System.out.print(c);
              }
            }
          }
          if (fCurrentEntity.position == fCurrentEntity.count) {
            System.out.print('^');
          }
          System.out.print('"');
        }
        System.out.print(']');
        System.out.print(" @ ");
        System.out.print(fCurrentEntity.lineNumber);
        System.out.print(',');
        System.out.print(fCurrentEntity.columnNumber);
      } else {
        System.out.print("*NO CURRENT ENTITY*");
      }
    }
  } // print()

  /**
   * Buffer used in entity manager to reuse character arrays instead
   * of creating new ones every time.
   *
   * @author Ankit Pasricha, IBM
   * @xerces.internal
   */
  private static class CharacterBuffer {

    /**
     * character buffer
     */
    private char[] ch;

    /**
     * whether the buffer is for an external or internal scanned entity
     */
    private boolean isExternal;

    public CharacterBuffer(boolean isExternal, int size) {
      this.isExternal = isExternal;
      ch = new char[size];
    }
  }


  /**
   * Stores a number of character buffers and provides it to the entity
   * manager to use when an entity is seen.
   *
   * @author Ankit Pasricha, IBM
   * @xerces.internal
   */
  private static class CharacterBufferPool {

    private static final int DEFAULT_POOL_SIZE = 3;

    private CharacterBuffer[] fInternalBufferPool;
    private CharacterBuffer[] fExternalBufferPool;

    private int fExternalBufferSize;
    private int fInternalBufferSize;
    private int poolSize;

    private int fInternalTop;
    private int fExternalTop;

    public CharacterBufferPool(int externalBufferSize, int internalBufferSize) {
      this(DEFAULT_POOL_SIZE, externalBufferSize, internalBufferSize);
    }

    public CharacterBufferPool(int poolSize, int externalBufferSize, int internalBufferSize) {
      fExternalBufferSize = externalBufferSize;
      fInternalBufferSize = internalBufferSize;
      this.poolSize = poolSize;
      init();
    }

    /**
     * Initializes buffer pool.
     **/
    private void init() {
      fInternalBufferPool = new CharacterBuffer[poolSize];
      fExternalBufferPool = new CharacterBuffer[poolSize];
      fInternalTop = -1;
      fExternalTop = -1;
    }

    /**
     * Retrieves buffer from pool.
     **/
    public CharacterBuffer getBuffer(boolean external) {
      if (external) {
        if (fExternalTop > -1) {
          return (CharacterBuffer) fExternalBufferPool[fExternalTop--];
        } else {
          return new CharacterBuffer(true, fExternalBufferSize);
        }
      } else {
        if (fInternalTop > -1) {
          return (CharacterBuffer) fInternalBufferPool[fInternalTop--];
        } else {
          return new CharacterBuffer(false, fInternalBufferSize);
        }
      }
    }

    /**
     * Returns buffer to pool.
     **/
    public void returnToPool(CharacterBuffer buffer) {
      if (buffer.isExternal) {
        if (fExternalTop < fExternalBufferPool.length - 1) {
          fExternalBufferPool[++fExternalTop] = buffer;
        }
      } else if (fInternalTop < fInternalBufferPool.length - 1) {
        fInternalBufferPool[++fInternalTop] = buffer;
      }
    }

    /**
     * Sets the size of external buffers and dumps the old pool.
     **/
    public void setExternalBufferSize(int bufferSize) {
      fExternalBufferSize = bufferSize;
      fExternalBufferPool = new CharacterBuffer[poolSize];
      fExternalTop = -1;
    }
  }

  /**
   * This class wraps the byte inputstreams we're presented with.
   * We need it because java.io.InputStreams don't provide
   * functionality to reread processed bytes, and they have a habit
   * of reading more than one character when you call their read()
   * methods.  This means that, once we discover the true (declared)
   * encoding of a document, we can neither backtrack to read the
   * whole doc again nor start reading where we are with a new
   * reader.
   *
   * This class allows rewinding an inputStream by allowing a mark
   * to be set, and the stream reset to that position.  <strong>The
   * class assumes that it needs to read one character per
   * invocation when it's read() method is inovked, but uses the
   * underlying InputStream's read(char[], offset length) method--it
   * won't buffer data read this way!</strong>
   *
   * @author Neil Graham, IBM
   * @author Glenn Marcy, IBM
   * @xerces.internal
   */

  protected final class RewindableInputStream extends InputStream {

    private InputStream fInputStream;
    private byte[] fData;
    private int fStartOffset;
    private int fEndOffset;
    private int fOffset;
    private int fLength;
    private int fMark;

    public RewindableInputStream(InputStream is) {
      fData = new byte[DEFAULT_XMLDECL_BUFFER_SIZE];
      fInputStream = is;
      fStartOffset = 0;
      fEndOffset = -1;
      fOffset = 0;
      fLength = 0;
      fMark = 0;
    }

    public void setStartOffset(int offset) {
      fStartOffset = offset;
    }

    public void rewind() {
      fOffset = fStartOffset;
    }

    public int read() throws IOException {
      int b = 0;
      if (fOffset < fLength) {
        return fData[fOffset++] & 0xff;
      }
      if (fOffset == fEndOffset) {
        return -1;
      }
      if (fOffset == fData.length) {
        byte[] newData = new byte[fOffset << 1];
        System.arraycopy(fData, 0, newData, 0, fOffset);
        fData = newData;
      }
      b = fInputStream.read();
      if (b == -1) {
        fEndOffset = fOffset;
        return -1;
      }
      fData[fLength++] = (byte) b;
      fOffset++;
      return b & 0xff;
    }

    public int read(byte[] b, int off, int len) throws IOException {
      int bytesLeft = fLength - fOffset;
      if (bytesLeft == 0) {
        if (fOffset == fEndOffset) {
          return -1;
        }

        /**
         * //System.out.println("fCurrentEntitty = " + fCurrentEntity );
         * //System.out.println("fInputStream = " + fInputStream );
         * // better get some more for the voracious reader... */

        if (fCurrentEntity.mayReadChunks || !fCurrentEntity.xmlDeclChunkRead) {

          if (!fCurrentEntity.xmlDeclChunkRead) {
            fCurrentEntity.xmlDeclChunkRead = true;
            len = fCurrentEntity.DEFAULT_XMLDECL_BUFFER_SIZE;
          }
          return fInputStream.read(b, off, len);
        }

        int returnedVal = read();
        if (returnedVal == -1) {
          fEndOffset = fOffset;
          return -1;
        }
        b[off] = (byte) returnedVal;
        return 1;

      }
      if (len < bytesLeft) {
        if (len <= 0) {
          return 0;
        }
      } else {
        len = bytesLeft;
      }
      if (b != null) {
        System.arraycopy(fData, fOffset, b, off, len);
      }
      fOffset += len;
      return len;
    }

    public long skip(long n)
        throws IOException {
      int bytesLeft;
      if (n <= 0) {
        return 0;
      }
      bytesLeft = fLength - fOffset;
      if (bytesLeft == 0) {
        if (fOffset == fEndOffset) {
          return 0;
        }
        return fInputStream.skip(n);
      }
      if (n <= bytesLeft) {
        fOffset += n;
        return n;
      }
      fOffset += bytesLeft;
      if (fOffset == fEndOffset) {
        return bytesLeft;
      }
      n -= bytesLeft;
            /*
            * In a manner of speaking, when this class isn't permitting more
            * than one byte at a time to be read, it is "blocking".  The
            * available() method should indicate how much can be read without
            * blocking, so while we're in this mode, it should only indicate
            * that bytes in its buffer are available; otherwise, the result of
            * available() on the underlying InputStream is appropriate.
            */
      return fInputStream.skip(n) + bytesLeft;
    }

    public int available() throws IOException {
      int bytesLeft = fLength - fOffset;
      if (bytesLeft == 0) {
        if (fOffset == fEndOffset) {
          return -1;
        }
        return fCurrentEntity.mayReadChunks ? fInputStream.available()
            : 0;
      }
      return bytesLeft;
    }

    public void mark(int howMuch) {
      fMark = fOffset;
    }

    public void reset() {
      fOffset = fMark;
      //test();
    }

    public boolean markSupported() {
      return true;
    }

    public void close() throws IOException {
      if (fInputStream != null) {
        fInputStream.close();
        fInputStream = null;
      }
    }
  } // end of RewindableInputStream class

  public void test() {
    //System.out.println("TESTING: Added familytree to entityManager");
    //Usecase1
    fEntityStorage.addExternalEntity("entityUsecase1", null,
        "/space/home/stax/sun/6thJan2004/zephyr/data/test.txt",
        "/space/home/stax/sun/6thJan2004/zephyr/data/entity.xml");

    //Usecase2
    fEntityStorage.addInternalEntity("entityUsecase2", "<Test>value</Test>");
    fEntityStorage.addInternalEntity("entityUsecase3", "value3");
    fEntityStorage.addInternalEntity("text", "Hello World.");
    fEntityStorage.addInternalEntity("empty-element", "<foo/>");
    fEntityStorage.addInternalEntity("balanced-element", "<foo></foo>");
    fEntityStorage.addInternalEntity("balanced-element-with-text", "<foo>Hello, World</foo>");
    fEntityStorage.addInternalEntity("balanced-element-with-entity", "<foo>&text;</foo>");
    fEntityStorage.addInternalEntity("unbalanced-entity", "<foo>");
    fEntityStorage.addInternalEntity("recursive-entity", "<foo>&recursive-entity2;</foo>");
    fEntityStorage.addInternalEntity("recursive-entity2", "<bar>&recursive-entity3;</bar>");
    fEntityStorage.addInternalEntity("recursive-entity3", "<baz>&recursive-entity;</baz>");
    fEntityStorage.addInternalEntity("ch", "&#x00A9;");
    fEntityStorage.addInternalEntity("ch1", "&#84;");
    fEntityStorage.addInternalEntity("% ch2", "param");
  }

} // class XMLEntityManager
